/* Copyright 2011 Rok Lenarčič 
 * 
 * This file is a part of MicroFn.
 * 
 * MicroFn is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * MicroFn is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/> */

package net.rlenar.microfn.func;

import java.util.Comparator;

import net.rlenar.microfn.IFn;

@SuppressWarnings("rawtypes")
public class FnComparator<E> implements Comparator<E> {
    protected IFn fn;
    protected int direction = 0;
    protected NullStrategy elementStrategy = NullStrategy.NULL_LAST;
    protected boolean asymmetric = false;
    protected Comparator<E> secondary = null;
    public FnComparator(IFn fn, boolean ascending) {
        this(fn, ascending, NullStrategy.NULL_LAST, null);
    }
    public FnComparator(IFn fn, boolean ascending, Comparator<E> secondary) {
        this(fn, ascending, NullStrategy.NULL_LAST, secondary);
    }    
    public FnComparator(IFn fn, boolean ascending, NullStrategy nullElementStrategy) {
        this(fn, ascending, nullElementStrategy, null);
    }    
    public FnComparator(IFn fn, boolean ascending, NullStrategy nullElementStrategy, Comparator<E> secondary) {
        this.fn = fn;
        direction = ascending ? 1 : -1;
        if (nullElementStrategy != null) {
        	this.elementStrategy = nullElementStrategy;
        }
        this.secondary = secondary;
    }
    protected FnComparator() {
    	;
    }

    /**
     * Returns new comparator which asymetrically treats compare arguments. The first argument of every compare
     * is used as-is and function is applied only on the second argument.
     * used as-is.
     * @return
     */
    public FnComparator<E> getAsymmetricComparator() {
    	FnComparator<E> comp = new FnComparator<E>();
    	comp.direction = direction;
    	comp.fn = fn;
    	comp.elementStrategy = elementStrategy;
    	comp.secondary = secondary;
    	comp.asymmetric = true;
    	return comp;
    }

	@SuppressWarnings("unchecked")
	public int compare(E o1, E o2) {
        Object result1 = fn.invoke(o1);
        Object result2 = asymmetric ? o2 : fn.invoke(o2);
        int result;
        if (result1 == null) {
            if (result2 == null) {
            	if (secondary == null) {
            		return 0;
            	} else {
            		return secondary.compare(o1, o2);
            	}
            } else {
                return elementStrategy.getReturnForFirstElNull(direction);
            }
        } else if (result2 == null) {
            return elementStrategy.getReturnForFirstElNull(direction) * -1;
        } else {
        	if (secondary == null) {
        		return ((Comparable)result1).compareTo(result2) * direction;	
        	} else {
        		result = ((Comparable)result1).compareTo(result2) * direction;
        		return result == 0 ? secondary.compare(o1, o2) : result;
        	}            
        }		
	}
	public enum NullStrategy {
		NULL_LAST(1, 0),
		NULL_FIRST(-1, 0),
		NULL_SMALLEST(0, -1),
		NULL_BIGGEST(0, 1);
		private int aprioriReturn;
		private int compareReturn;
		private NullStrategy(int aprioriReturn, int compareReturn) {
			this.aprioriReturn = aprioriReturn;
			this.compareReturn = compareReturn;
		}
		protected int getReturnForFirstElNull(int direction) {
			return aprioriReturn + compareReturn * direction;
		}
	}
	public Comparator<E> getSecondary() {
		return secondary;
	}
	public void setSecondary(Comparator<E> secondary) {
		this.secondary = secondary;
	}
	public void setAscending(boolean ascending) {
		direction = ascending ? 1 : -1;
	}
	public boolean getAscending() {
		return direction == 1;
	}
}
